package sa.Master.Server;

import sa.Master.common.Functions.*;
import sa.Master.common.NetworkMessages.Login.*;
import sa.Master.common.NetworkMessages.Validated.*;
import sa.Master.common.Classes.*;
import sa.Master.Server.Modules.*;

import java.io.*;
import java.net.*;

public class Server extends ServerSocket {

    public static enum Field {
        personal,
        group
    }
    
    private File serverRoot;
    private UserInfoManager userInfoManager;
    private PropertyManager propertyManager;
    private ImageManager imageManager;

    private final int maxActiveSessionCount;
    private int activeSessionCount;
    private long totalSessionCount;

    private final int timeout;
    
    /**
     * Server initialization, do the following jobs:
     *     Start listening on specified port
     *     Load root Directory
     *     Login: Load User Validating information (in module class UserInfoManager)
     * 
     * @param portNum Listening port number
     * @param rootDir Root directory of the server
     * @param maxSession Maximum active session at a time
     * @param timeout Maximum waiting time when client haven't send request.
     * 
     * @throws IOException on:
     *     1. ServerSocket open failure
     * @throws FileNotFoundException on:
     *     1. Specified root directory"rootDir" is not a directory
     *     2. User Validating information not found
     */
    public Server(int portNum, File rootDir, int maxSession, int timeout) throws IOException, FileNotFoundException {
        super(portNum);
        this.maxActiveSessionCount = maxSession;
        this.totalSessionCount = 0l;
        this.timeout = timeout;
        
        // check availability of rootDir
        if (!rootDir.isDirectory()) {
            throw new FileNotFoundException(String.format("Server: %s: Not a directory.", rootDir.toString()));
        } else if (!rootDir.canWrite()) {
            throw new FileNotFoundException(String.format("Server: %s: Server root not writable.", rootDir.toString()));
        } else if (!rootDir.canRead()) {
            throw new FileNotFoundException(String.format("Server: %s: Server root not readable.", rootDir.toString()));
        } else {
            this.serverRoot = rootDir;
        }

        // loads User Validating Module
        this.userInfoManager = new UserInfoManager(this.serverRoot);
        // loads Property Management Module
        this.propertyManager = new PropertyManager(this.serverRoot, timeout);
        // loads Image manager Module
        this.imageManager = new ImageManager(this.serverRoot);
        // add ctrl+c handler
        Runtime.getRuntime().addShutdownHook(new Thread(new CtrlC_Handler(this)));
        
    }

    /**
     * Outside interface: Start server listening
     * 
     * Accept new client connections.
     * Set Socket timeout.
     * Block new connection when active connection count exceeds maxActiveSessionCount.
     */
    public void load() {

        while (true) {
            Socket s = null;
            try {

                // Block client if reached max session count.
                while (activeSessionCount>=maxActiveSessionCount) {
                    Thread.sleep(1000);
                }

                // If serve tension is low, run permanence process.
                // LEARNING initialization of non-static inner class FilePersist.
                if (activeSessionCount<=((int)(maxActiveSessionCount*0.4))) {
                    persist();
                }

                // Start to accept new client.
                s = this.accept();
                // Accepted a new connection.
                s.setSoTimeout(timeout);
                new Thread(new Session(s)).start();
                
                LoggingUtilities.sysout(String.format("Server: Active session count: %d / %d.", activeSessionCount, maxActiveSessionCount));

            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public void persist() {
        new Thread(this.userInfoManager.new FilePersist()).start();
    }

    /**
     * Serve one client.
     * 
     * Every session is a thread on server.
     * When a new client requires connection, a Session is then created to serve that client.
     * When the connection closes, the Session is over.
     * 
     * @author whsu
     */
    private class Session implements Runnable {

        private long sessionID;
        private Socket clientSocket;
        private String userName;
        private RequestHandler requestHandler;

        public Session(Socket clientSocket) {

            this.clientSocket = clientSocket;
            this.sessionID = totalSessionCount++;
            this.requestHandler = null;  // assign request handler and userName after login
            this.userName = null;

            activeSessionCount++;

        }

        public void run() {

            try {
                // Login validation
                if (!loginValidate()) {
                    return;
                }
                // Ready for requests
                while (true) {
                    acceptRequest();
                }

            } catch (IOException e) {
                LoggingUtilities.syserr(String.format("Session %d: Timed out.", this.sessionID));
            } catch (ClassNotFoundException e) {
                LoggingUtilities.syserr(e.getMessage());
            } catch (LogoutException e) {
                LoggingUtilities.sysout(String.format("Session %d: Log out successfully.", this.sessionID));
            } finally {
                try {this.clientSocket.close();} catch (IOException e) {}
                activeSessionCount--;
            }

        }

        private boolean loginValidate() throws IOException, ClassNotFoundException {

            ObjectInputStream requestInputStream = null;
            ObjectOutputStream responseOutputStream = null;
            LoginRequest clientRequest = null;

            // Nertwork io process
            try {
                requestInputStream = new ObjectInputStream(this.clientSocket.getInputStream());
                responseOutputStream = new ObjectOutputStream(this.clientSocket.getOutputStream());
                clientRequest = (LoginRequest) requestInputStream.readObject();
            } catch (IOException e) {
                throw new IOException(String.format("Session %d: Login: Socket failure.", this.sessionID));
            } catch (ClassNotFoundException e) {
                throw new ClassNotFoundException(String.format("Session %d: Login: Client request failure.", this.sessionID));
            }

            LoginResponse.ReturnStatus status = userInfoManager.validate(clientRequest.userName, clientRequest.password);
            try {
                // TODO append avatar if login successfully
                responseOutputStream.writeObject(new LoginResponse(status));
            } catch (IOException e) {
                throw new IOException(String.format("Session %d: Login: Response failure.", this.sessionID));
            }

            if (status == LoginResponse.ReturnStatus.success) {
                // get User Info
                this.userName = clientRequest.userName;
                this.requestHandler = new RequestHandler(userInfoManager, propertyManager, imageManager, this.userName);
                LoggingUtilities.sysout(String.format("Session %d: Login: successful.", this.sessionID));
            } else {
                LoggingUtilities.syserr(String.format("Session %d: Login: failure: %s.",
                                                 this.sessionID,
                                                 status == LoginResponse.ReturnStatus.wrong_pwd ? "Username/Password incorrect": "Blocked"));
            }

            return status == LoginResponse.ReturnStatus.success;

        }

        private void acceptRequest() throws IOException, LogoutException {

            ObjectInputStream requestInputStream = null;
            ObjectOutputStream responseOutputStream = null;
            Request request = null;
            Response response = null;

            try {
                requestInputStream = new ObjectInputStream(this.clientSocket.getInputStream());
                responseOutputStream = new ObjectOutputStream(this.clientSocket.getOutputStream());
                request = (Request) requestInputStream.readObject();
                response = requestHandler.getResponse(request);
                responseOutputStream.writeObject(response);

                if (!(response.success)) {
                    LoggingUtilities.syserr(String.format("Session %d: %s", this.sessionID, response.returnData.get("text")));
                } else {
                    LoggingUtilities.sysout(String.format("Session %d: %s %s: successful.", this.sessionID, request.action, request.object));
                }
            } catch (IOException | ClassNotFoundException e) {
                throw new IOException(String.format("Session %d: response/request transmission: Network error.", this.sessionID));
            }

        }

    }
}
